package session

import (
	"gitee.com/chencanxin/goose/cache"
	"sync"
	"errors"
	"net/http"
	"net/url"
	"github.com/dgrijalva/jwt-go"
	"time"
	"encoding/json"
	"github.com/dgrijalva/jwt-go/request"
)

type SessionManager struct {

	sessionName string

	storeProvider cache.Cache

	expireTime int64

	sp *sync.Pool

	httOnly bool

	path string

	domain string

	cookieLifeTime int


}

type Session struct {
	Sid string
	Value interface{}
	Expire int64
}

const (
	SecretKey = "This is a SecretKey"   //emmm,it should be generated in other ways or another usage mode
)

var ErrCookieValueNil = errors.New("session: nil returned")

var ErrTokenValueNil = errors.New("token value nil")

func init(){

}

func NewSessionManager(sessionName ,providerName,config string,expireTime int64,httpOnly bool,path string,domain string,cookieLifeTime int)(sm *SessionManager,err error){
	if sessionName == "" {
		return nil,errors.New("sessionName is empty")
	}

	if providerName == ""{
		return nil,errors.New("providerName is empty,it could be 'redis' or 'storm' and so on")
	}

	if config == ""{
		return nil,errors.New("storeProvide config is empty,detail config information can refer cache config")
	}

	if expireTime <= 0 {
		return nil,errors.New("maxExpire must >=0")
	}

	sm = new(SessionManager)
	sm.sessionName = sessionName
	sm.cookieLifeTime = cookieLifeTime
	sm.path = path
	sm.httOnly = httpOnly
	sm.domain = domain
	sm.storeProvider,err = cache.CacheManager.NewCache(providerName,config)
	if err!=nil{
		return
	}

	sm.expireTime = expireTime

	sm.sp =  &sync.Pool{
		New:func() interface{}{
			return &Session{}
		},
	}

	return
}

//Set will encapsulates value in session and store to cache
//the value's expire will be sessionManager.expireTime
func (sm *SessionManager)Set(key string,value interface{})(err error){
	session := sm.getSessionInstance()
	session.Sid = key
	session.Value = value
	session.Expire = sm.expireTime

	err = sm.storeProvider.SetEx(sm.sessionName,key,session,sm.expireTime)
	if err!=nil{
		return err
	}

	return
}

//Set will encapsulates value in session and store to cache
//expire can set the value's expireTime
func (sm *SessionManager)SetEx(key string,value interface{},expire int64)(err error){

 	session := sm.getSessionInstance()
	session.Sid = key
	session.Value = value
	session.Expire = sm.expireTime

	err = sm.storeProvider.SetEx(sm.sessionName,key,session,expire)
	if err!=nil{
		return err
	}

 	sm.sp.Put(session)

	return
}

//Get will set exist value to parameter value if key match
//It will return ErrCacheValueNil
func (sm *SessionManager)Get(key string,value interface{})(err error){

	session := sm.getSessionInstance()

	session.Value = value
	if err = sm.storeProvider.Get(sm.sessionName,key,&session);err!=nil{
		return
	}

	sm.sp.Put(session)

	return
}

//Destroy del value that matches the key
func (sm *SessionManager)Destroy(key string)(err error){
	if err = sm.storeProvider.Delete(sm.sessionName,key);err!=nil{
		return err
	}
	return
}

//if value that matches the key exist,return true,else return false
func (sm *SessionManager)IsExist(key string) (b bool) {
	b,_=sm.storeProvider.IsExist(sm.sessionName,key)
	return
}

//Del All Session that matches sessionName
func (sm *SessionManager)ClearAll()(err error){
	if err = sm.storeProvider.ClearAll(sm.sessionName);err!=nil{
		return
	}
	return
}

//set session value to parameter value by get key from r.Cookie
//if cookie is nil or cookie.value is nil ,return ErrCookieValueNil
//if session value is nil,return ErrCacheValueNil
func (sm *SessionManager)GetThroughCookie(w http.ResponseWriter, r *http.Request,value interface{})(err error){
	cookie ,err := r.Cookie(sm.sessionName)
	if err!=nil || cookie.Value==""{
		return ErrCookieValueNil
	}

	if err:=sm.Get(cookie.Value,value);err!=nil{
		return err
	}
	return
}

//SetToCookieAndCache will encapsulates value in session and store to cache and set key to cookie
//cookie's attribute will be set by SessionManager's cookieLifeTime,httOnly ...
func (sm *SessionManager)SetAndCreateCookie(w http.ResponseWriter,r *http.Request,key string,value interface{})(err error){

	if err = sm.setToCookie(w,r,key);err!=nil{
		return err
	}

	if err = sm.Set(key,value);err!=nil{
		return err
	}
	return
}

//like SetToCookieAndCache,the expire can set session's expire time
func (sm *SessionManager)SetAndCreateCookieEx(w http.ResponseWriter,r *http.Request,key string,value interface{},expire int)(err error){
	if err = sm.setToCookie(w,r,key);err!=nil{
		return err
	}

	if err = sm.SetEx(key,value,int64(expire));err!=nil{
		return
	}
	return
}

//GetFromToken will set value from token to parameter value.
//if token is invalid (it maybe expire ,SecretKey matches fail or request is't carry token),return err
func (sm *SessionManager)GetFromToken(w http.ResponseWriter, r *http.Request,value interface{})(err error){

	token,err := request.ParseFromRequest(r,request.AuthorizationHeaderExtractor,
		func(token *jwt.Token)(interface{},error){
			return []byte(SecretKey),nil
	})
	if err!=nil{
		return err
	}

	if val,ok := token.Header["val"];ok{
		if err := json.Unmarshal([]byte(val.(string)),value);err!=nil{
			return err
		}
	}else{
		return ErrTokenValueNil
	}
	return
}

//SetToToken wile set value to token and set into Response's Header "Authorization"
func(sm *SessionManager)CreateToken(w http.ResponseWriter,r *http.Request,value interface{},arg...interface{})(err error){
	token := jwt.New(jwt.SigningMethodHS256)
	jsonValue,err := json.Marshal(value)
	if err!=nil{
		return err
	}

	claims := make(jwt.MapClaims)
	var expire int64
	if len(arg) > 0 {
		if e,ok := arg[0].(int);ok && e > 0{
			expire = int64(e)
		}else{
			expire = sm.expireTime
		}
	}else{
		expire = sm.expireTime
	}

	token.Header["val"] = string(jsonValue)
	claims["exp"] = time.Now().Add(time.Second*time.Duration(expire)).Unix()
	token.Claims = claims

	tokenString,err := token.SignedString([]byte(SecretKey))
	if err!=nil{
		return err
	}

	w.Header().Set("Authorization",tokenString)

	return
}

//get session instance from session pool
func (sm *SessionManager)getSessionInstance()(session *Session){
	sn:= sm.sp.Get()
	var ok bool

	session,ok = sn.(*Session)
	if !ok{
		session = &Session{}
	}

	return
}

//set key to cookie value
func (sm *SessionManager)setToCookie(w http.ResponseWriter,r *http.Request,key string)(err error){
	cookie ,err := r.Cookie(sm.sessionName)
	if err!=nil || cookie.Value == ""{
		cookie = &http.Cookie{
			Name:sm.sessionName,
			Value:url.QueryEscape(key),
			Path :sm.path,
			HttpOnly:sm.httOnly,
			Domain:sm.domain,
			MaxAge:sm.cookieLifeTime,
		}
		err = nil
	}else{
		oldKey,err := url.QueryUnescape(cookie.Value)
		if err!=nil{
			return err
		}

		if err = sm.Destroy(oldKey);err!=nil{
			return err
		}
		cookie.Name = sm.sessionName
		cookie.Value = url.QueryEscape(key)
		cookie.HttpOnly = sm.httOnly
		cookie.MaxAge =sm.cookieLifeTime
		cookie.Path = sm.path
		cookie.Domain = sm.domain
	}

	http.SetCookie(w,cookie)
	r.AddCookie(cookie)

	return
}



// ManagerConfig define the session config
type ManagerConfig struct {
	CookieName              string `json:"cookieName"`
	EnableSetCookie         bool   `json:"enableSetCookie,omitempty"`
	Gclifetime              int64  `json:"gclifetime"`
	Maxlifetime             int64  `json:"maxLifetime"`
	DisableHTTPOnly         bool   `json:"disableHTTPOnly"`
	Secure                  bool   `json:"secure"`
	CookieLifeTime          int    `json:"cookieLifeTime"`
	ProviderConfig          string `json:"providerConfig"`
	Domain                  string `json:"domain"`
	SessionIDLength         int64  `json:"sessionIDLength"`
	EnableSidInHTTPHeader   bool   `json:"EnableSidInHTTPHeader"`
	SessionNameInHTTPHeader string `json:"SessionNameInHTTPHeader"`
	EnableSidInURLQuery     bool   `json:"EnableSidInURLQuery"`
}